/*
  IniFile: a class for reading and writing initialisation files.

  There are three lists: one for the file, one for the section and one for the
  items.

  The file list holds the line and the position and character replaced by null
  (because the section and item are not duplicated, they just point into the
  line).

  The section list holds a pointer to its name and a pointer to the first item.

  The item list holds a pointer to the line its in, and pointers to its item
  and value. The dummy last item holds a pointer to the last item's line - this
  is for inserting a new line.

  Jason Hood. Public domain.
  29 November, 1996.
   4 February, 1997 - Added GetItem() and GetInt().
  19 November to    - Corrected Open() bug when comments were the last lines;
   3 December	      added dirty, stylistic changes, corrected bug when
		       creating a file;
		      renamed from MyConfigFile;
		      added sections;
		      used bool, rather than TWS' bool;
		      added Flush() function;
		      wrote the description;
		      added GetBool();
		      added RemoveSection() and RemoveItem();
		      added RetrieveSections() and RetrieveItems();
		      added GetDouble().
   5 January, 2001  - Corrected bug when creating a blank line (use strdup("")
		       instead of = "", to correctly free memory).
*/

#include "inifile.h"
#include <stdio.h>

#define isaspace(c) ((c) == ' ' || (c) == '\t')


/* --------------------------------   Open   --------------------------------

  Read filename into memory and create the section and item lists. If it cannot
  be opened return false. If a file is already opened, close it.

  It is assumed there is enough memory to hold the file.

*/

bool IniFile::Open(const char* filename)
{
  if (inifile) Close();

  inifile   = strdup(filename); 	// Remember the filename for writing
  sectionList.append(Section());	// The default section ("[]")

  FILE* file = fopen(filename, "rt");
  if (!file) return false;

  char	buffer[1025];			// 1K line length should be plenty
  ItemList* itemlist = &sectionList.begin()->items;
  Line*     line;
  Item	    item;

  while (fgets(buffer, sizeof(buffer), file) != NULL)
  {
    buffer[strlen(buffer) - 1] = 0;	// Strip the newline
    line = lineList.append(Line());
    line->line = strdup(buffer);

    // Skip the leading spaces
    for (item.item = line->line; isaspace(*item.item); ++item.item);
    if (*item.item == '[') AddSection(lineList.node(), item.item+1, itemlist);
    else if (strchr("#%;\\/'\"|$@!:", *item.item) == NULL &&
	     (item.value = strchr(item.item, '=')) != NULL)
    {
      item.line = lineList.node();
      // Find the end of the item and terminate it
      for (line->end = item.value-1; isaspace(*line->end); --line->end);
      line->ch = *(++line->end); *line->end = 0;
      // Find the start of the value
      for (++item.value; isaspace(*item.value); ++item.value);
      itemlist->append(item);
    }
  }

  fclose(file);
  return true;
}


/* --------------------------------   Flush   -------------------------------

  Write the list of lines to the file (provided an item's been changed).
  Returns false if no file has been opened, or can't create it.

*/

bool IniFile::Flush()
{
  if (!inifile) return false;
  if (!dirty)	return true;

  FILE* file = fopen(inifile, "wt");
  if (!file) return false;

  foritems (Line, lineList)
  {
    if (item->end) *item->end = item->ch;  // Replace the overwritten character
    fputs(item->line, file); fputc('\n', file);
    if (item->end) *item->end = 0;	   // Put the terminator back
  }

  fclose(file);
  dirty = false;			// Nice and clean again
  return true;
}


/* --------------------------------   Close   -------------------------------

  Flush the file and free the allocated memory.
  Returns false if no file has been opened, or the file could not be written.
  In the latter case, the memory is still preserved.

*/

bool IniFile::Close()
{
  if (!Flush()) return false;

  free(inifile); inifile = NULL;
  lineList.empty();
  foritems (Section, sectionList) item->items.empty();
  sectionList.empty();

  return true;
}


/* ----------------------------   FindSection	-----------------------------

  Return a pointer to section. If it doesn't exist, return NULL.

*/

IniFile::Section* IniFile::FindSection(const char* section)
{
  Section* ptr = sectionList.begin();

  if (section == NULL || *section == 0) return ptr;

  while ((ptr = ++sectionList) != 0)
  {
    if (!stricmp(ptr->section, section)) return ptr;
  }

  return NULL;
}


/* ----------------------------    AddSection	-----------------------------

  Find the section name and add it to the list, if it doesn't already exist.

  section should be pointing to the first character after the initial [.

*/

void IniFile::AddSection(LineNode* line, char* section, ItemList*& items)
{
  char *name, *end;

  // See if there's an ending ]
  for (end  = section + strlen(section) - 1; end >= section && isaspace(*end);
       --end);
  if (*end != ']') return;      // Nup, so ignore the line

  // Skip the leading spaces to get the start of the section
  for (name = section; isaspace(*name); ++name);
  // Skip the trailing spaces to get the end of the section
  for (--end; end >= section && isaspace(*end); --end);
  ++end;
  line->item.end = end; 	// Set the end of the section,
  line->item.ch  = *end;	//  remember the last character and
  *end		 = 0;		//  set it to null, to terminate the name

  Section* temp = FindSection(name);
  if (temp == NULL)
  {
    items = &sectionList.append(Section(name, line))->items;
  }
  else
  {
    *end	   = line->item.ch;	// No point in terminating the name
    line->item.end = NULL;
    temp->line	   = line;
    items	   = &temp->items;
  }
}


/* ------------------------------   FindItem   ------------------------------

  Return a pointer to the item in section. If it doesn't exist, return NULL.

*/

IniFile::Item* IniFile::FindItem(ItemList& section, const char* item)
{
  foritemsv (Item, itm, section)
  {
    if (!stricmp(itm->item, item)) return itm;
  }

  return NULL;
}


/* -------------------------------   GetItem   ------------------------------

  Return the value associated with item in section. If the item (or section)
  does not exist, or no file is opened, return defalt. If item exists more
  than once, the FIRST one will be returned.

*/

char* IniFile::GetItem(const char* section, const char* item, char* defalt)
{
  Section* items = FindSection(section);
  if (items == NULL) return defalt;

  Item* itm = FindItem(items->items, item);
  if (itm == NULL) return defalt;

  return itm->value;
}


/* -------------------------------   GetInt   -------------------------------

  As for GetItem(), but returns an integer.

*/

int IniFile::GetInt(const char* section, const char* item, int defalt)
{
  char* value = GetItem(section, item, "");
  return (*value) ? atoi(value) : defalt;
}


/* -----------------------------   GetDouble   ------------------------------

  As for GetItem(), but returns a double.

*/

double IniFile::GetDouble(const char* section, const char* item, double defalt)
{
  char* value = GetItem(section, item, "");
  return (*value) ? atof(value) : defalt;
}


/* ------------------------------   GetBool   -------------------------------

  As for GetItem(), but returns a bool.

  This function recognizes non-zero numbers, True, Yes and On. Anything else
  will be false. (Strings that start with numbers (eg. "123abc") will be true.)

*/

bool IniFile::GetBool(const char* section, const char* item, bool defalt)
{
  char* value = GetItem(section, item, "");
  if (*value == 0) return defalt;

  if (atoi(value) != 0	      ||
      !stricmp(value, "True") ||
      !stricmp(value, "Yes")  ||
      !stricmp(value, "On")) return true;

  return false;
}


/* ------------------------------   WriteItem   -----------------------------

  Add "item = value" if it doesn't exist, otherwise preserve spacing and
  overwrite the old value. Eg:

  "     item    =   value"

  becomes

  "     item    =   newvalue"

  If item is duplicated, only the FIRST one will be modified.
  value is assumed to contain no leading space.

  If the section doesn't exist, add a blank line (unless the file is being
  created) and "[ section ]".

  Returns false if no file is open.

*/

bool IniFile::WriteItem(const char* section, const char* item,
					     const char* value)
{
  Line*     line;
  Section*  sect;
  ItemList* items;
  Item*     itm;

  sect = FindSection(section);
  if (sect == NULL)			// The section doesn't exist
  {
    if (!lineList.is_empty())
    {
      line = lineList.append(Line());
      line->line = strdup("");
    }
    line = lineList.append(Line());
    line->line = (char*)malloc(strlen(section) + 5);
    sprintf(line->line, "[ %s ]", section);
    *(line->end = line->line+2 + strlen(section)) = 0;
    line->ch = ' ';
    sect  = sectionList.append(Section(line->line+2, lineList.node()));
    items = &sect->items;
    itm   = NULL;
  }
  else
  {
    items = &sect->items;
    itm = FindItem(*items, item);
  }

  if (itm == NULL)			// The item doesn't exist
  {
    dirty = true;

    // If the default section doesn't exist, add this item at the start of the
    // file. Add a blank line after it, if it's not the only line.
    if (sect->section == NULL && !items->count())
    {
      line = lineList.prepend(Line());
      sect->line = lineList.node();
      if (lineList.count() > 1) lineList.insert(Line())->line = "";
    }
    else
    {
      line = lineList.insert(Line(), sect->line);
      sect->line = lineList.node();
    }
    itm 	= items->append(Item());
    itm->line	= lineList.node();
    line->line	= (char*)malloc(strlen(item) + 3 + strlen(value) + 1);
    sprintf(line->line, "%s = %s", item, value);
    itm->item	= line->line;
    *(line->end = line->line + strlen(item)) = 0;
    itm->value	= line->end + 3;
    line->ch	= ' ';
  }
  else if (strcmp(itm->value, value))	// Only if the value is different
  {
    dirty = true;
    if (strlen(itm->value) < strlen(value))	// Allocate a longer line
    {
      line = &itm->line->item;
      int item_ofs  = itm->item  - line->line,
	  end_ofs   = line->end  - line->line,
	  value_ofs = itm->value - line->line;
      // If the old value is empty ("item =") then add the space.
      bool space = (itm->value[-1] == '=');
      line->line = (char*)realloc((void*)line->line,
				  value_ofs + space + strlen(value) + 1);
      itm->item  = line->line + item_ofs;
      line->end  = line->line + end_ofs;
      itm->value = line->line + value_ofs;
      if (space) *(itm->value++) = ' ';
    }
    strcpy(itm->value, value);
  }
  return true;
}


/* ---------------------------	 RemoveSection	 ----------------------------

  Remove a section, and all items in it, from the file. If the section name is
  duplicated, only the first will be removed. If the section name is preceded
  by a blank line, delete that too.

*/

bool IniFile::RemoveSection(const char* section)
{
  Section* sect = FindSection(section);
  if (sect == NULL) return false;

  foritems (Item, sect->items) lineList.remove(item->line);
  if (sect->section == NULL) sect->items.empty();
  else
  {
    lineList.move(sect->sect);
    if (--lineList != 0) if (*lineList.here()->line == 0) lineList.remove();
    lineList.remove(sect->sect);
    sectionList.remove();
  }

  dirty = true;
  return true;
}


/* -----------------------------   RemoveItem	-----------------------------

  Remove the item from the section. If the item is duplicated, only the first
  will be removed. If it's the only item in the section, the section itself
  still remains.

*/

bool IniFile::RemoveItem(const char* section, const char* item)
{
  Section* sect = FindSection(section);
  if (sect == NULL) return false;
  Item* itm = FindItem(sect->items, item);
  if (itm == NULL) return false;

  lineList.remove(itm->line);
  sect->items.remove();

  dirty = true;
  return true;
}


/* --------------------------	RetrieveSections   --------------------------

  Create an array containing all the section names in the file (except the
  default section) and return the count. It is the responsibility of the
  caller to delete the memory.

*/

size_t IniFile::RetrieveSections(const char**& array)
{
  if (sectionList.count() < 2) return 0;

  array = new (const char*)[sectionList.count() - 1];
  if (array == 0) return 0;

  size_t   pos	= 0;
  Section* sect = sectionList.begin();
  while ((sect = ++sectionList) != 0)
  {
    array[pos++] = sect->section;
  }

  return pos;
}


/* ---------------------------	 RetrieveItems	 ----------------------------

  Create an array containing all the item names for section and return the
  count. It is the responsibility of the caller to delete the memory.

*/

size_t IniFile::RetrieveItems(const char* section, const char**& array)
{
  Section* sect = FindSection(section);
  if (sect == NULL) return 0;

  array = new (const char*)[sect->items.count()];
  if (array == 0) return 0;

  size_t pos = 0;
  foritems (Item, sect->items) array[pos++] = item->item;

  return pos;
}
